// Copyright (C) 2009 Jesse Jones
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using MmmDoc;
using Mono.Cecil;
using System;
using System.Collections.Generic;

namespace MmmDoc.Database
{
	/// <summary>This namespace contains format independent information about the
	/// types and methods of the assemblies for which we are generating documentation.</summary>
	internal sealed class NamespaceDoc
	{
	}
	
	/// <summary>The in-memory representation of the documentation.</summary>
	internal sealed class Document
	{
		internal Document()
		{
		}
		
		/// <summary>Adds an assembly from which types and methods will be extracted for the documentation.</summary>
		internal void AddAssembly(AssemblyDefinition assembly)
		{
			m_assemblies.Add(assembly);
		}
		
		internal void OnFinishedAdding()
		{
			m_types = DoGetTypes();
			Array.ForEach(m_types, t =>t.Init(this, m_cookie));
		}
		
		/// <summary>Returns all of the types which we are documenting.</summary>
		public TypeDef[] Types
		{
			get {return m_types;}
		}
		
		/// <summary>Used to ensure that only Document creates <see cref = "Database.TypeDef"/> instances.</summary>
		[Pure]
		internal bool IsValidCookie(object obj)
		{
			return ReferenceEquals(obj, m_cookie);
		}
		
		/// <summary>Returns true if the method should be documented.</summary>
		/// <param name = "method">May be null.</param>
		internal bool IsVisible(MethodDefinition method)
		{
			bool visible = false;
			
			if (method != null)
			{
				if (method.IsGetter || method.IsSetter || !DoIsGeneratedCode(method))
				{
					MethodAttributes access = method.Attributes & MethodAttributes.MemberAccessMask;
					switch (access)
					{
						case MethodAttributes.Family:
						case MethodAttributes.FamORAssem:
						case MethodAttributes.Public:
							visible = true;
							break;
						
						case MethodAttributes.FamANDAssem:
						case MethodAttributes.Assem:
							visible = Settings.Visibility != Visibility.External;
							break;
						
						case MethodAttributes.Private:
							visible = Settings.Visibility == Visibility.All;
							break;
						
						case MethodAttributes.Compilercontrolled:
							visible = false;
							break;
						
						default:
							Contract.Assert(false, "bad access: " + access);
							break;
					}
				}
			}
			
			return visible;
		}
		
		/// <summary>Returns true if the field should be documented.</summary>
		internal bool IsVisible(FieldDefinition field)
		{
			bool visible = false;
			
			if (!DoIsGeneratedCode(field))
			{
				FieldAttributes access = field.Attributes & FieldAttributes.FieldAccessMask;
				switch (access)
				{
					case FieldAttributes.Public:
					case FieldAttributes.Family:
					case FieldAttributes.FamORAssem:
						visible = true;
						break;
					
					case FieldAttributes.Assembly:
					case FieldAttributes.FamANDAssem:
						visible = Settings.Visibility != Visibility.External;
						break;
					
					case FieldAttributes.Private:
						visible = Settings.Visibility == Visibility.All;
						break;
					
					default:
						Contract.Assert(false, "bad access: " + access);
						break;
				}
			}
			
			return visible;
		}
		
		#region Private Methods
		private TypeDef[] DoGetTypes()
		{
			var types = new List<TypeDef>();
			
			foreach (AssemblyDefinition assembly in m_assemblies)
			{
				foreach (ModuleDefinition module in assembly.Modules)
				{
					foreach (TypeDefinition type in module.Types)
					{
						// Note that nested types are listed in the module as well as the type.
						if (DoIsVisible(type) || type.Name == "NamespaceDoc")
							types.Add(new TypeDef(this, type, m_cookie));
					}
				}
			}
			
			return types.ToArray();
		}
		
		private bool DoIsVisible(TypeDefinition type)
		{
			bool visible = false;
			
			if (!DoIsGeneratedCode(type))
			{
				TypeAttributes vis = type.Attributes & TypeAttributes.VisibilityMask;
				switch (vis)
				{
					case TypeAttributes.Public:
					case TypeAttributes.NestedPublic:
					case TypeAttributes.NestedFamily:
					case TypeAttributes.NestedFamORAssem:
						visible = true;
						break;
					
					case TypeAttributes.NotPublic:
					case TypeAttributes.NestedAssembly:
					case TypeAttributes.NestedFamANDAssem:
						visible = Settings.Visibility != Visibility.External;
						break;
					
					case TypeAttributes.NestedPrivate:
						visible = Settings.Visibility == Visibility.All;
						break;
					
					default:
						Contract.Assert(false, "bad visiblity: " + vis);
						break;
				}
			}
			
			return visible;
		}
		
		// Based on the gendarme code.
		private bool DoIsGeneratedCode(TypeReference type)
		{
			if (type.HasCustomAttributes)
				if (type.Module.Assembly.Runtime >= TargetRuntime.NET_2_0)
					if (DoHasGeneratedAtribute(type.CustomAttributes))
						return true;
			
			switch (type.Name[0])
			{
				case '<': 			// e.g. <Module>, <PrivateImplementationDetails>
				case '$': 			// e.g. $ArrayType$1 nested inside <PrivateImplementationDetails>
					return true;
			}
			
			if (type.IsNested)
				return DoIsGeneratedCode(type.DeclaringType);
				
			return false;
		}
		
		private bool DoIsGeneratedCode(MethodDefinition method)
		{
			if (method.HasCustomAttributes)
				if (DoHasGeneratedAtribute(method.CustomAttributes))
					return true;
			
			return false;
		}
		
		private bool DoIsGeneratedCode(FieldDefinition field)
		{
			if (field.HasCustomAttributes)
				if (DoHasGeneratedAtribute(field.CustomAttributes))
					return true;
			
			return false;
		}
		
		private bool DoHasGeneratedAtribute(CustomAttributeCollection attrs)
		{
			foreach (CustomAttribute attr in attrs)
			{
				string fullName = attr.Constructor.DeclaringType.FullName;
				if (fullName == "System.CodeDom.Compiler.GeneratedCodeAttribute")
					return true;
				
				else if (fullName == "System.Runtime.CompilerServices.CompilerGeneratedAttribute")
					return true;
			}
			
			return false;
		}
		#endregion
		
		#region Fields
		private List<AssemblyDefinition> m_assemblies = new List<AssemblyDefinition>();
		private TypeDef[] m_types;
		private object m_cookie = new object();
		#endregion
	}
}
